﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MacomberMapSystem.Common.Serialization;
using MacomberMapSystem.Common.Database;
using System.Xml;
using System.Collections.Specialized;
using MacomberMapSystem.Common.Internals;

namespace MacomberMapSystem.Common.CIM
{
    /// <summary>
    /// This class holds information on a CIM element difference
    /// </summary>
    public class CIM_Element_Difference
    {
        #region Variable declarations
        /// <summary>The rdf:ID of the element being changed</summary>
        [MM_Serializable_Target(MM_Serializable_Target.enumTargetType.FieldValue)]
        public CIM_RdfID rdfID;

        /// <summary>The model associated with the element difference</summary>
        public MM_Database_Model CIM;

        /// <summary>The type of the element</summary>
        [MM_Serializable_Target(MM_Serializable_Target.enumTargetType.FieldValue)]
        public Byte Type;

        /// <summary>The name of the element type</summary>
        public virtual String TypeName
        {
            get { return CIM.Types[Type]; }
            set
            {
                for (int a = 0; a < CIM.Types.Count; a++)
                    if (CIM.Types[a] == value)
                    {
                        Type = (byte)a;
                        return;
                        
                    }
            }
        }

        /// <summary>The collection of added attributes</summary>
        [MM_Serializable_Target(MM_Serializable_Target.enumTargetType.FieldValue)]
        public MM_CIM_SortedCollection AddedAttributes;

        /// <summary>The collection of removed attributes</summary>
        [MM_Serializable_Target(MM_Serializable_Target.enumTargetType.FieldValue)]
        public MM_CIM_SortedCollection RemovedAttributes;

        /// <summary>The collection of added outgoing links</summary>
        public List<CIM_RdfID> AddedLinks = new List<CIM_RdfID>(1);

        /// <summary>The collection of removed outgoing links</summary>
        public List<CIM_RdfID> RemovedLinks = new List<CIM_RdfID>(1);

        /// <summary>The type of change being performed</summary>
        [MM_Serializable_Target(MM_Serializable_Target.enumTargetType.FieldValue)]
        public enumChangeType ChangeType;
        #endregion

        #region Enumerations
        /// <summary>
        /// The types of changes that are avaialble
        /// </summary>
        [Flags]
        public enum enumChangeType
        {
            /// <summary>The change type is new/uninitialized</summary>
            Unknown = 0,
            /// <summary>An element has been added</summary>
            ElementAddition = 1,
            /// <summary>An element has been removed</summary>
            ElementDeletion = 2,
            /// <summary>An attribute has been added</summary>
            AttributeAddition = 4,
            /// <summary>An attribute has been deleted</summary>
            AttributeDeletion = 8,
            /// <summary>A link has been added</summary>
            LinkAddition = 16,
            /// <summary>A link has been deleted</summary>
            LinkDeletion = 32,
        }
        #endregion

        #region Initialization
        /// <summary>
        /// Initialize a new element difference
        /// </summary>
        /// <param name="rdfID"></param>
        /// <param name="Reverse"></param>
        /// <param name="Forward"></param>
        /// <param name="CIM"></param>
        public CIM_Element_Difference(CIM_RdfID rdfID, XmlElement Reverse, XmlElement Forward, MM_Database_Model CIM)
        {            
            this.CIM = CIM;
            this.rdfID = rdfID;

            //If this is a new rdf:ID, treat it as an addition
            if (CIM.FromRdfID(rdfID) == null)
                this.ChangeType |= enumChangeType.ElementAddition;

            //Parse our reverse linkages
            if (Reverse != null)
            {                
                this.TypeName = Reverse.Name;
                if (Reverse.HasChildNodes)
                {
                    RemovedAttributes = new MM_CIM_SortedCollection();
                    List<CIM_RdfID> RemovedrdfIDs = new List<CIM_RdfID>();
                    foreach (XmlElement xChild in Reverse.ChildNodes)
                        if (xChild.HasAttribute("rdf:resource"))
                        if (xChild.Attributes["rdf:resource"].Value.StartsWith("#"))
                        {
                            RemovedLinks.Add(new CIM_RdfID(xChild.Attributes["rdf:resource"].Value.Substring(1)));                            
                            RemovedAttributes.Add(xChild.Name, xChild.Attributes["rdf:resource"].Value);
                            ChangeType |= enumChangeType.LinkDeletion;
                        }
                        else
                        {
                            RemovedAttributes.Add(xChild.Name, xChild.Attributes["rdf:resource"].Value);
                            ChangeType |= enumChangeType.AttributeDeletion;
                        }
                        else
                        {
                            RemovedAttributes.Add(xChild.Name, xChild.InnerText);
                            ChangeType |= enumChangeType.AttributeDeletion;
                        }                    
                }
                else
                    ChangeType |= enumChangeType.ElementDeletion;
            }

            //Parse our forward linkages
            if (Forward != null)
            {
                this.TypeName = Forward.Name;
                if (Forward.HasChildNodes)
                {
                    AddedAttributes = new MM_CIM_SortedCollection();                    
                    List<CIM_RdfID> AddedrdfIDs = new List<CIM_RdfID>();
                    foreach (XmlElement xChild in Forward.ChildNodes)
                        if (xChild.HasAttribute("rdf:resource"))
                            if (xChild.Attributes["rdf:resource"].Value.StartsWith("#"))
                            {
                                AddedLinks.Add(new CIM_RdfID(xChild.Attributes["rdf:resource"].Value.Substring(1)));
                                AddedAttributes.Add(xChild.Name, xChild.Attributes["rdf:resource"].Value);
                                ChangeType |= enumChangeType.LinkAddition;
                            }
                            else
                            {
                                AddedAttributes.Add(xChild.Name, xChild.Attributes["rdf:resource"].Value);
                                ChangeType |= enumChangeType.AttributeAddition;
                            }   
                        else
                        {
                            AddedAttributes.Add(xChild.Name, xChild.InnerText);
                            ChangeType |= enumChangeType.AttributeAddition;
                        }                    
                }               
            }
        }

        /// <summary>
        /// Initialize a new CIM element difference
        /// </summary>
        /// <param name="rdfID"></param>
        /// <param name="CIM"></param>
        /// <param name="ChangeType"></param>
        public CIM_Element_Difference(CIM_RdfID rdfID, MM_Database_Model CIM, enumChangeType ChangeType)
        {
            this.rdfID = rdfID;
            this.CIM = CIM;
            this.ChangeType = ChangeType;
        }
        #endregion

        #region Exporting
        /// <summary>
        /// Write out our reverse information, if any
        /// </summary>
        /// <param name="xW"></param>
        public void WriteReverse(XmlTextWriter xW)
        {
            if ((ChangeType | enumChangeType.ElementDeletion) == enumChangeType.ElementDeletion)
            {
                xW.WriteStartElement(TypeName);
                xW.WriteAttributeString("rdf:ID", rdfID.ToString());
                xW.WriteEndElement();
            }
            else if ((ChangeType | enumChangeType.AttributeDeletion) == enumChangeType.AttributeDeletion)
            {
                xW.WriteStartElement(TypeName);
                xW.WriteAttributeString("rdf:about", rdfID.ToString());
                foreach (KeyValuePair<string, string> kvp in RemovedAttributes)
                    if (kvp.Value.StartsWith("#"))
                    {
                        xW.WriteStartElement(kvp.Key);
                        xW.WriteAttributeString("rdf:resource", kvp.Value);
                        xW.WriteEndElement();
                    }
                    else
                        xW.WriteAttributeString(kvp.Key, kvp.Value);
                xW.WriteEndElement();
            }
        }

        /// <summary>
        /// Write out our forward information, if any
        /// </summary>
        /// <param name="xW"></param>
        public void WriteForward(XmlTextWriter xW)
        {
            if ((ChangeType | enumChangeType.ElementAddition) == enumChangeType.ElementAddition)
            {
                xW.WriteStartElement(TypeName);
                xW.WriteAttributeString("rdf:ID", rdfID.ToString());
                xW.WriteEndElement();
            }
            else if ((ChangeType | enumChangeType.AttributeAddition) == enumChangeType.AttributeAddition)
            {
                xW.WriteStartElement(TypeName);
                xW.WriteAttributeString("rdf:about", rdfID.ToString());
                foreach (KeyValuePair<string, string> kvp in AddedAttributes)
                    if (kvp.Value.StartsWith("#"))
                    {
                        xW.WriteStartElement(kvp.Key);
                        xW.WriteAttributeString("rdf:resource", kvp.Value);
                        xW.WriteEndElement();
                    }
                    else
                        xW.WriteAttributeString(kvp.Key, kvp.Value);
                xW.WriteEndElement();
            }
        }
        #endregion

        #region Identification
        /// <summary>
        /// Report an easy-to-read string
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            if ((ChangeType | enumChangeType.ElementAddition) == enumChangeType.ElementAddition)
                return "Add " + TypeName + " " + rdfID.ToString();
            else
                return ChangeType.ToString() + " " + CIM.FromRdfID(rdfID).ToString();
        }
        #endregion

        #region Difference creator
        /// <summary>
        /// Load a new difference file
        /// </summary>
        /// <param name="xDoc"></param>
        /// <param name="CIM"></param>
        /// <returns></returns>
        /// <param name="RemoveExisting">Whether to remove existing instances of the specified types.</param>
        public static CIM_Element_Difference[] LoadDifferenceFile(XmlDocument xDoc, MM_Database_Model CIM, bool RemoveExisting)
        {
            //Load our file into memory, pulling in forward and reverse changes
            Dictionary<CIM_RdfID, XmlElement[]> TempElems = new Dictionary<CIM_RdfID, XmlElement[]>();
            List<String> IncludedTypes = new List<string>();

            //If the file is a proper delta file, pull everything in. Otherwise, assume it's a standard CIM snippet, and pull it in as forwards.
            XmlElement DifferenceModel = xDoc["rdf:RDF"]["dm:DifferenceModel"];
            if (DifferenceModel != null)
            {
                foreach (XmlElement xElem in DifferenceModel["dm:reverseDifferences"].ChildNodes)
                    TempElems.Add(GetID(xElem), new XmlElement[] { xElem, null });
                foreach (XmlElement xElem in DifferenceModel["dm:forwardDifferences"].ChildNodes)
                {
                    if (TempElems.ContainsKey(GetID(xElem)))
                        TempElems[GetID(xElem)][1] = xElem;
                    else
                        TempElems.Add(GetID(xElem), new XmlElement[] { null, xElem });
                    if (!IncludedTypes.Contains(xElem.Name))
                        IncludedTypes.Add(xElem.Name);
                }
            }
            else
                foreach (XmlElement xElem in xDoc.DocumentElement.ChildNodes)
                {
                    if (TempElems.ContainsKey(GetID(xElem)))
                        TempElems[GetID(xElem)][1] = xElem;
                    else
                        TempElems.Add(GetID(xElem), new XmlElement[] { null, xElem });
                    if (!IncludedTypes.Contains(xElem.Name))
                        IncludedTypes.Add(xElem.Name);
                }            

            //Now, produce our outgoing list
            Dictionary<CIM_RdfID, CIM_Element_Difference> OutDiffs = new Dictionary<CIM_RdfID, CIM_Element_Difference>();
            foreach (KeyValuePair<CIM_RdfID, XmlElement[]> kvp in TempElems)
                OutDiffs.Add(kvp.Key,new CIM_Element_Difference(kvp.Key,kvp.Value[0], kvp.Value[1], CIM));


            CIM_Element_Difference cDiff;
            if (RemoveExisting)
                foreach (String strType in IncludedTypes)
                    foreach (CIM_Element ElemToRemove in CIM.ElementsOfType(strType))
                        if (OutDiffs.TryGetValue(ElemToRemove.rdfID, out cDiff))
                        {
                            cDiff.ChangeType |= enumChangeType.AttributeDeletion;
                            if (ElemToRemove.Attributes.Count > 0)
                                cDiff.RemovedAttributes = new MM_CIM_SortedCollection();
                            if (ElemToRemove.OutLinks.Length > 0)
                                cDiff.RemovedLinks = new List<CIM_RdfID>();
                            foreach (KeyValuePair<String, String> kvp in ElemToRemove.AttributeList)
                                cDiff.RemovedAttributes.Add(kvp.Key, kvp.Value);
                            foreach (CIM_Link OutLink in ElemToRemove.OutLinks)
                            {
                                cDiff.ChangeType |= enumChangeType.LinkDeletion;
                                cDiff.RemovedLinks.Add(OutLink.rdfID);
                            }
                        }
                        else
                            OutDiffs.Add(ElemToRemove.rdfID, new CIM_Element_Difference(ElemToRemove.rdfID, CIM, enumChangeType.ElementDeletion));


            return OutDiffs.Values.ToArray();
        }

        /// <summary>
        /// Determine the rdf:ID of an Xml Element
        /// </summary>
        /// <param name="xElem"></param>
        /// <returns></returns>
        public static CIM_RdfID GetID(XmlElement xElem)
        {
            return new CIM_RdfID(xElem.HasAttribute("rdf:ID") ? xElem.Attributes["rdf:ID"].Value : xElem.Attributes["rdf:about"].Value);

        }
        #endregion

        #region Static methods
        /// <summary>
        /// Modify an attribute
        /// </summary>
        /// <param name="Element"></param>
        /// <param name="AttributeName"></param>
        /// <param name="AttributeValue"></param>
        public static void ModifyAttribute(CIM_Element Element, String AttributeName, String AttributeValue)
        {
            CIM_Element_Difference cDiff;
            if (!Element.CIM.ModelChanges.TryGetValue(Element.rdfID, out cDiff))
                Element.CIM.ModelChanges.Add(Element.rdfID, cDiff = new CIM_Element_Difference(Element.rdfID, Element.CIM, enumChangeType.Unknown));

            //Find all attriubtes that need to be removed
            foreach (KeyValuePair<String, String> kvp in Element.Attributes)
                if (String.Equals(kvp.Key, AttributeName, StringComparison.CurrentCultureIgnoreCase))
                {
                    cDiff.RemovedAttributes.Add(kvp.Key, kvp.Value);
                    cDiff.ChangeType |= enumChangeType.AttributeDeletion;
                }
                      
            cDiff.ChangeType |= enumChangeType.AttributeDeletion;
            cDiff.AddedAttributes.Add(AttributeName, AttributeValue);
        }

        /// <summary>
        /// Modify an attribute
        /// </summary>
        /// <param name="Element"></param>
        /// <param name="AttributeName"></param>
        /// <param name="AttributeOldValue"></param>
        /// <param name="AttributeNewValue"></param>
        public static void ModifyAttribute(CIM_Element Element, String AttributeName, String AttributeOldValue, String AttributeNewValue)
        {
            CIM_Element_Difference cDiff;
            if (!Element.CIM.ModelChanges.TryGetValue(Element.rdfID, out cDiff))
                Element.CIM.ModelChanges.Add(Element.rdfID, cDiff = new CIM_Element_Difference(Element.rdfID, Element.CIM, enumChangeType.Unknown));

            //Remove attributes as needed
            if (Element.AttributeList.Contains(AttributeName, AttributeOldValue))
            {
                cDiff.RemovedAttributes.Add(AttributeName, AttributeOldValue);
                cDiff.ChangeType |= enumChangeType.AttributeDeletion;
            }

            cDiff.ChangeType |= enumChangeType.AttributeDeletion;
            cDiff.AddedAttributes.Add(AttributeName, AttributeNewValue);
        }

        /// <summary>
        /// Remove a linkage between elements
        /// </summary>
        /// <param name="Elem1"></param>
        /// <param name="Elem2"></param>
        public static void RemoveLinkage(CIM_Element Elem1, CIM_Element Elem2)
        {
            foreach (CIM_Link OutLink in Elem1.OutLinks)
                if (OutLink.TargetID == Elem2.ID)
                    RemoveLinkage(Elem1, Elem2.rdfID);
            foreach (CIM_Link OutLink in Elem2.OutLinks)
                if (OutLink.TargetID == Elem1.ID)
                    RemoveLinkage(Elem2, Elem1.rdfID);            
        }

        /// <summary>
        /// Remove the linkage from Element 1 to rdf:ID 2
        /// </summary>
        /// <param name="Elem1"></param>
        /// <param name="rdfID2"></param>        
        private static void RemoveLinkage(CIM_Element Elem1, CIM_RdfID rdfID2)
        {
            CIM_Element_Difference cDiff;
            if (!Elem1.CIM.ModelChanges.TryGetValue(Elem1.rdfID, out cDiff))
            Elem1.CIM.ModelChanges.Add(Elem1.rdfID, new CIM_Element_Difference(Elem1.rdfID, Elem1.CIM, enumChangeType.Unknown));            
            foreach (KeyValuePair<String,String> kvp in Elem1.AttributeList)
                if (String.Equals(kvp.Value, "#" + rdfID2.ToString(), StringComparison.CurrentCultureIgnoreCase))
                {
                    cDiff.RemovedAttributes.Add(kvp.Key, kvp.Value);
                    cDiff.RemovedLinks.Add(rdfID2);
                    cDiff.ChangeType |= enumChangeType.AttributeDeletion;
                    cDiff.ChangeType |= enumChangeType.LinkDeletion;
                }

        }

        /// <summary>
        /// Remove an element from the model, as well as all of its outgoing linkages (to ensure the processed inlinks of the other elements show properly)
        /// </summary>
        /// <param name="ElemToRemove"></param>
        public static void RemoveElement(CIM_Element ElemToRemove)
        {
            CIM_Element_Difference cDiff;
            if (ElemToRemove.CIM.ModelChanges.TryGetValue(ElemToRemove.rdfID, out cDiff))
                cDiff.ChangeType |= enumChangeType.ElementDeletion;                    
            else
                ElemToRemove.CIM.ModelChanges.Add(ElemToRemove.rdfID, cDiff = new CIM_Element_Difference(ElemToRemove.rdfID, ElemToRemove.CIM, enumChangeType.ElementDeletion));
            
            foreach (KeyValuePair<String,String> kvp in ElemToRemove.AttributeList)
                if (kvp.Value.StartsWith("#"))
                {
                    cDiff.ChangeType |= enumChangeType.AttributeDeletion | enumChangeType.LinkDeletion;
                    cDiff.RemovedLinks.Add(new CIM_RdfID(kvp.Value.Substring(1)));
                    cDiff.RemovedAttributes.Add(kvp.Key, kvp.Value);
                }                    
        }
        #endregion
    }
}
